查看原文
其他

Twitter 的 17 条 Compose 开发规范和检查:帮你避坑~

鸿洋
2024-08-24

The following article is from TechMerger Author 小虾米君

翻译自:

https://twitter.github.io/compose-rules/rules/

前言

对于大型团队来说,刚开始采用 Compose 开发的时候,会面临很多的挑战。尤其每个开发者对 Compose 的认知不同:接触的时间或长或短、开发的水平也参差不齐。
Twitter 计划通过创建一套 Compose rules 来解决这些痛点。经过一段时间的探索之后,Twitter 推出了一套自定义的 Compose 静态检查 rules,可以确保开发者编写的 Composables 函数避免一些常见的错误。

的确,Compose 技术有很多超能力,但也存在很多容易犯的错(坑),这时候上面的静态检测 rules 便可以派上用场了。我们期望这些 rules 可以在正式 review 代码之前,便帮助开发者检测出尽可能多的、潜在的 Compose 使用问题,从而促进 Compose 技术的健康发展!

1State 状态相关

1. 保持状态的提升

有一种设计理念叫做“单向数据流”,它的特征是:状态下降、事件上升。Compose 技术也是建立在这种单向数据流理念上的,可以概括为:状态向下流动,事件向上触发。
为了实现这一点,Compose 主张尽量保持《状态的提升》,从而使得大部分的可组合函数都是不具备状态,这样做有很多好处,比如更加解耦、易于测试。
  • 状态的提升:https://developer.android.com/jetpack/compose/state#state-hoisting
在实践中,还有一些注意点需要留意:
  • 不要向下传递 ViewModels 或来自 DI 带来的实例。
  • 不要向下传递 State<Foo>  MutableState<Bar> 实例。
取而代之的是,可以向 Composable 函数传递相关的数据以及用于回调的 lambda。
更多信息可以查看:
  • Compose 和状态文档:https://developer.android.com/jetpack/compose/state
该 rule 的名称和源码:vm-forwarding-check -> ComposeViewModelForwarding.kt

2. 记住状态

通过 mutableStateOf 或任何其他的 State builder 构建 State 实例的时候,需要注意:确保代码中 remember 了这个 State 实例。否则,在 Composable 函数重组时,就会构建出一个新的 State 实例。
该 rule 的名称和源码:remember-missing-check -> ComposeRememberMissing.kt

3. 使用 @Immutable

Compose 编译器会去推断相关数据的不可变性 immutable 和稳定性 stable,但有时候这种判断会出错,这就会造成 UI 界面会多做些不必要的刷新工作。所以,如果想让编译器将某个类视为 "不可变"的,最好直接给该类使用 @Immutable 注解。
更多信息可以参考:
  • 不可变文档:https://developer.android.com/reference/kotlin/androidx/compose/runtime/Immutable
  • 可组合度量:https://chris.banes.dev/composable-metrics

4. 不使用不稳定的集合声明

Kotlin 中,集合 Collections 被定义为接口类型,例如:List<T>, Map<T>, Set<T>。而他们的内部数据是否可变,是无法保证的。
举个例子:
val list:List<String> = mutableListOf<String>()

变量 list 在声明的时候采用的类型是 val,意味着不可重新赋值,但其实 list 内部成员是可以改变的。
Compose 编译器在处理这种类型的变量时,虽然看到了 val 声明,但因无法准确判断其内容是否会发生变化,便会将该变量判定为不稳定。
要想强制让编译器将该集合判定为真正的"不可变",有这么几个方案,比如:采用 ImmutableList 接口的类型进行声明。
 val list:ImmutableList<String> = persistentListOf<String>()

或者,将集合封装在一个带注解 @Immutable 的稳定类中。
@Immutable
data class StringList(val items: List<String>)
// ...
val list:StringList = StringList(yourList)

注意:最好使用 Kotlinx 中定义的不可变集合接口类型和方法。因为你可能也发现了,虽然后者通过注解强调了它是不可变的,但其实其内部的 List 仍然是可变的。


更多信息可以参考:
  • Jetpack Compose 稳定性详解:https://medium.com/androiddevelopers/jetpack-compose-stability-explained-79c10db270c8
  • Kotlinx 不可变集合:https://github.com/Kotlin/kotlinx.collections.immutable

该 rule 的名称和源码:unstable-collections -> ComposeUnstableCollections.kt

2Composables 可组合函数相关

5. 不采用可变类型作函数参数

本条规则是由上面提到的“状态提升”规则延伸出来的。


“状态提升”规则里我们提到状态是向下流动的,可事实上很多开发者会情不自禁地将可变的 State 传递到函数里直接去改变它的值。但这是一种违反模式的做法,因为它破坏了状态向下流动、事件向上触发的模式。
值的改变作为一种事件,它应当在函数 API(lambda 回调)中进行构建。这样做的一个重要理由是:Compose 里极容易发生更新了可变对象却没有触发重组的情况。因为如果没能触发重组,可组合函数就不会被自动更新,进而无法反映更新后的值到 UI 上去。
常常被传递给可组合函数作为可变参数的,包括但不仅限于:ArrayList<T>、MutableState<T> 和 ViewModel。
该 rule 的名称和源码:mutable-params-check -> ComposeMutableParameters.kt

6. 不要同时发射布局又返回结果

可组合函数应该只发射布局内容,或者只返回某个结果。但不能两个都做,这样会显得混乱。
另外,如果可组合函数需要为调用方提供额外的界面控制,则这些控制逻辑或回调应作为参数由调用方提供给可组合函数。
更多信息可以参考:
  • Compose API guidelines:https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#emit-xor-return-a-value
该 rule 的名称和源码:content-emitter-returning-values-check -> ComposeMultipleContentEmitters.kt

注意:你可以将 composeEmitters 添加到 Detekt 规则配置中,或将 compose_emitters 添加到 ktlint 中的 .editorconfig 配置中。


7. 不要发射多片段的布局节点

一个可组合函数可以不发射或者只发射 1 段布局片段,切忌过多。因为可组合函数应当具备内聚性,而不应依赖于调用的函数。
下面是一个错误的示范:InnerContent() 函数会发出多个布局节点,并设想它该被 Column 的布局所调用。
Column {
    InnerContent()
}

@Composable
private fun InnerContent() {
    Text(...)
    Image(...)
    Button(...)
}

然而,InnerContent 也可以很容易地从 Row 中调用,这将打破所有假设。相反,InnerContent 应具有内聚性,并且本身应发出一个布局节点:
@Composable
private fun InnerContent() {
    Column {
        Text(...)
        Image(...)
        Button(...)
    }
}

与传统的 View 视图系统相比,Compose 布局嵌套的成本要低得多,因此开发者不需要去刻意地简化界面层级,甚至牺牲了正确性。
这条规则有一个小小的例外,那就是当可组合函数被定义为一个特定作用域扩展函数的时候,比如如下:
@Composable
private fun ColumnScope.InnerContent() {
    Text(...)
    Image(...)
    Button(...)
}

这段代码将多个片段的布局有效地绑定到了从 Column 中调用的函数,尽管允许这样编码,但其实不推荐。
该 rule 的名称和源码:multiple-emitters-check -> ComposeMultipleContentEmitters.kt

8. 恰当命名 CompositionLocals 变量

给 CompositionLocal 命名时,应使用形容词 "Local"作为前缀,后面跟一个描述性的名词,描述其持有的值。
这样就能非常清晰地知道某个值来自某个 CompositionLocal 。鉴于这些都是隐含的依赖关系,我们尽量在命名层面将它们清晰地表露出来。
更多信息可以参考:
  • Naming CompositionLocals:https://android.googlesource.com/platform/frameworks/support/+/androidx-main/compose/docs/compose-api-guidelines.md#naming-compositionlocals
该 rule 的名称和源码:compositionlocal-naming -> ComposeCompositionLocalNaming.kt

9. 恰当命名 multipreview 注解

当自定义用于多个预览的注解时,其命名应使用 Previews 作为后缀。给这些注解明确的命名,可以确保在使用的时候,开发者能清楚地知道它们是 @Preview 的多个组合。
更多信息可以参考:
  • Multipreview annotations:https://developer.android.com/jetpack/compose/tooling#preview-multipreview
该 rule 的名称和源码:preview-naming -> ComposePreviewNaming.kt

10. 恰当命名可组合函数

当可组合函数是 Unit 类型的时候,其命名应当以大写字母开头。它们被视为声明性实体,在组合中可以存在、也可以不存在,因此需要遵循类 class 的命名规则。
但是,带返回值的可组合函数应该以小写字母开头,应遵循《Kotlin Coding Conventions》中关于函数命名的规则。
  • Kotlin Coding Conventions:https://kotlinlang.org/docs/reference/coding-conventions.html#function-names
更多信息可以参考:
  • Naming Unit @Composable functions as entities:https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#naming-unit-composable-functions-as-entities
  • Naming @Composable functions that return values:https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#naming-composable-functions-that-return-values
该 rule 的名称和源码:naming-check -> ComposeNaming.kt

11. 有序定义可组合函数的参数

在 Kotlin 中编写函数的时候,一个好的做法是先写必选参数,然后再写可选参数(即有默认值的参数)。这样做的话,我们可以最大限度地减少需要明确写出参数的次数,提高编码效率。
Modifier 通常会占据可选参数的第 1 个槽位,便可以为开发者提供统一的编码规范:即开发者可以始终提供一个 Modifier 实例作为元素调用的位置参数。
更多信息可以参考:
  • Kotlin default arguments:https://kotlinlang.org/docs/functions.html#default-arguments
  • Modifier docs:https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier
  • Elements accept and respect a Modifier parameter:https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#why-8
该 rule 的名称和源码:param-order-check -> ComposeParameterOrder.kt

12. 显示声明依赖关系

ViewModels

在设计可组合函数的时候,我们应尽量明确它们之间的依赖关系。如果在可组合函数的主体中,从 DI 获取 ViewModel 或某个实例,就等于隐式地产生了依赖关系,可这样做的缺点是难以测试、也难以复用。
为了解决这个问题,你应该在可组合函数中将这些依赖关系作为默认值注入。让我们举例说明:
@Composable
private fun MyComposable() {
    val viewModel = viewModel<MyViewModel>()
}


上述这种可组合函数里,依赖关系是隐式的。在测试时,你需要用某种方式伪造 viewModel 的内部结构,以便获取你想要的 ViewModel 实例。
但是,如果将其改为通过函数参数传递这些实例,就可以在测试中直接提供所需的实例,不再需要额外的工作。这样做还有一个好处,就是可以在函数定义里明确声明其对外存在依赖关系。
@Composable
private fun MyComposable(
    viewModel: MyViewModel = viewModel()
,
) { ... }


该 rule 的名称和源码:vm-injection-check -> ComposeViewModelInjection.kt

CompositionLocals

CompositionLocal 使可组合函数的行为更难推理。由于它们会创建隐式依赖关系,调用它们的可组合函数需要确保每个 CompositionLocal 的值都得到满足。
虽然它们并不常见,但也有《合法用例》,因此本规则提供了一个允许列表,开发者可以将自己的 "CompositionLocal" 名称添加到该列表中,这样规则脚本就会将他们除外。
  • 合法用例:https://developer.android.com/jetpack/compose/compositionlocal#deciding
该 rule 的名称和源码:compositionlocal-allowlist -> CompositionLocalAllowlist.kt
注意:要将自定义的 CompositionLocal 添加到允许列表中,可以在 Detekt 的规则配置中添加 allowedCompositionLocals 或在 ktlint 的 .editorconfig 中添加 allowed_composition_locals 。


13. 声明仅支持预览的函数为 private

当一个可组合函数仅仅拥有 @Preview 注解,不会在实际的用户界面中调用的话,它不需要被声明为 public 的。同时,为防止其他开发者在不知情的情况下使用了它,我们应该将其可见性限制为private。
该 rule 的名称和源码:preview-public-check -> ComposePreviewPublic.kt
注意:如果您使用 Detekt,这可能会与 Detekt 的 UnusedPrivateMember 规则冲突。请务必将 Detekt 的 ignoreAnnotated 配置设置为['预览'],以便与此规则兼容。

3Modifiers 修饰符相关

14. 尽量提供 Modifier 参数

为了实现开发者将逻辑和行为自由附加到 Compose UI 上的目的,Compose 推出了组合而非继承的理念。Modifier 则是实现这个理念的最重要组件。
Modifier 对所有公共的 UI 组件都很重要,通过它,调用者便可以按照自己的意愿定制组件的各种组合。
更多信息可以参考:
  • Always provide a Modifier parameter:https://chris.banes.dev/posts/always-provide-a-modifier
该 rule 的名称和源码:modifier-missing-check -> ComposeModifierMissing.kt

15. 不重复使用 Modifiers

传入的 Modifier 实例应由可组合函数内单个布局节点使用。如果所提供的 Modifiers 被不同层级的多个可组合函数所使用,可能会发生预期外的行为。
在下面的示例中,可组合函数定义了一个公共的 Modifier 参数,内部将其传递给根节点的 Column 组件。但同时在调用每个子组件的时候也传递了了该参数,并在基础上添加了一些额外的 Modifier:
@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column(modifier) {
        Text(modifier.clickable(), ...)
        Image(modifier.size(), ...)
        Button(modifier, ...)
    }
}

其实不建议这样编码,参数里的 Modifier 实例仅应该被用到 Column 组件上。子组件应使用通过空的 Modifier 单例对象新建的 Modifier 实例。
@Composable
private fun InnerContent(modifier: Modifier = Modifier) {
    Column(modifier) {
        Text(Modifier.clickable(), ...)
        Image(Modifier.size(), ...)
        Button(Modifier, ...)
    }
}

该 rule 的名称和源码:modifier-reused-check -> ComposeModifierReused.kt

16. Modifier 应当具备默认的参数

可将 Modifier 作为参数应用于所代表的整个组件的可组合函数,应命名该参数为 modifier,并分配 Modifier 参数的默认值。它应当声明为参数列表中的第 1 个可选参数,且位于所有必选参数(尾部的 lambda 参数除外)之后,但应位于任何其他具有默认值的参数之前。
在可组合函数的实现中,可组合函数所需的任何默认 Modifier 都应位于 Modifier 参数值之后,并将 Modifier 保留为默认参数值。
更多信息可以参考:
  • Modifier documentation:https://developer.android.com/reference/kotlin/androidx/compose/ui/Modifier
该 rule 的名称和源码:modifier-without-default-check -> ComposeModifierWithoutDefault.kt

17. 避免使用扩展函数构建 Modifier

不推荐在可组合函数里使用常用的扩展函数去构造 Modifier 实例,因为它们会导致不必要的重组。为避免该情况,推荐使用 Modifier.composed,因为它会将重组限制在 Modifier 实例上,而不是针对整个函数 tree。
而且 Composed Modifier 可能在组合之外创建出来、跨组件之间共享、并声明为顶层常量,这使得它们比在可组合函数里调用扩展函数创建的 Modifier 更灵活,也更容易避免意外的跨组件共享状态数据。
更多信息可以参考:
  • Modifier extensions:https://developer.android.com/reference/kotlin/androidx/compose/ui/package-summary#extension-functions
  • Composed modifiers in Jetpack Compose by Jorge Castillo:https://jorgecastillo.dev/composed-modifiers-in-jetpack-compose
  • Composed modifiers in API guidelines:https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#composed-modifiers

4结语


如上的 rules 是 Twitter 使用 Compose 开发多年以来,不断结合官方文档和实战总结出来的宝贵经验。
如果想要使用该规则去检测代码是否合适,可以使用 ktlint、Detekt 来导入规则和部署检查:
  • Using with ktlint:https://twitter.github.io/compose-rules/ktlint
  • Using with Detekt:https://twitter.github.io/compose-rules/detekt
下一篇文章,我将采用上述 rules 对我在几年前 Compose 还未正式发布时写的《Compose 复刻 Flappy Bird》项目进行检查实操,敬请期待!
  • https://github.com/ellisonchan/ComposeBird

规则文档的开源地址

https://github.com/twitter/compose-rules

译者备注

不像 Java、Kotlin 这种由来已久的语言,已经有很多成熟的 rules,并被广泛认可和部署到大大小小的项目当中。
而像 Compose 这种新兴的、落地不多的项目来说,很多规则、建议都还在摸索当中,像 Twitter 这种大厂能够将开发心得无私地总结和开源出来,是非常难能可贵的。
可惜我在该项目的 issues 列表里看到一则提问:
The future of this project?


Twitter 的员工回复说:因为该 repo 核心人员的离职,本 repo 的未来不太明朗。
如今它最近的一次提交截止在 2023 年 1 月!
我衷心希望开发者们能向这个 repo 持续地贡献力量,让它壮大下去。
如果哪一天这个规范的部分或全部内容被广泛接受、纳入到 Compose 官方 rules 当中,那对 Compose 技术、Android UI 技术的发展来说,都是意义非凡的事情。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

Android Bitmap 图像处理:变亮、变灰、反色
我用 Bézier 曲线创造了一个机器人
凡猿修仙传:斩杀HardwareRenderer.nSetStopped ANR


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存